App.require(function () {
    var PERLIN_YWRAPB = 4;
    var PERLIN_YWRAP = 1 << PERLIN_YWRAPB;
    var PERLIN_ZWRAPB = 8;
    var PERLIN_ZWRAP = 1 << PERLIN_ZWRAPB;
    var PERLIN_SIZE = 4095;

    var perlin_octaves = 4; // default to medium smooth
    var perlin_amp_falloff = 0.5; // 50% reduction/octave

    var scaled_cosine = function (i) {
        return 0.5 * (1.0 - Math.cos(i * Math.PI));
    };
    var perlin;

    var lcg = (function () {
        var m = 4294967296;
        var a = 1664525;
        var c = 1013904223;
        var seed, z;

        return {
            setSeed: function (val) {
                z = seed = (val == null ? Math.random() * m : val) >>> 0;
            },
            getSeed: function () {
                return seed;
            },
            rand: function () {
                z = (a * z + c) % m;
                return z / m;
            }
        };
    })();

    function noise(x, y, z) {
        y = y || 0;
        z = z || 0;

        if (perlin == null) {
            perlin = new Array(PERLIN_SIZE + 1);
            for (var i = 0; i < PERLIN_SIZE + 1; i++) {
                perlin[i] = Math.random();
            }
        }

        if (x < 0) {
            x = -x;
        }
        if (y < 0) {
            y = -y;
        }
        if (z < 0) {
            z = -z;
        }

        var xi = Math.floor(x),
            yi = Math.floor(y),
            zi = Math.floor(z);
        var xf = x - xi,
            yf = y - yi,
            zf = z - zi;
        var rxf, ryf;
        var r = 0,
            ampl = 0.5;

        var n1, n2, n3;

        for (var o = 0; o < perlin_octaves; o++) {
            var of = xi + (yi << PERLIN_YWRAPB) + (zi << PERLIN_ZWRAPB);

            rxf = scaled_cosine(xf);
            ryf = scaled_cosine(yf);

            n1 = perlin[ of & PERLIN_SIZE];
            n1 += rxf * (perlin[( of +1) & PERLIN_SIZE] - n1);
            n2 = perlin[( of +PERLIN_YWRAP) & PERLIN_SIZE];
            n2 += rxf * (perlin[( of +PERLIN_YWRAP + 1) & PERLIN_SIZE] - n2);
            n1 += ryf * (n2 - n1);

            of += PERLIN_ZWRAP;
            n2 = perlin[ of & PERLIN_SIZE];
            n2 += rxf * (perlin[( of +1) & PERLIN_SIZE] - n2);
            n3 = perlin[( of +PERLIN_YWRAP) & PERLIN_SIZE];
            n3 += rxf * (perlin[( of +PERLIN_YWRAP + 1) & PERLIN_SIZE] - n3);
            n2 += ryf * (n3 - n2);

            n1 += scaled_cosine(zf) * (n2 - n1);

            r += n1 * ampl;
            ampl *= perlin_amp_falloff;
            xi <<= 1;
            xf *= 2;
            yi <<= 1;
            yf *= 2;
            zi <<= 1;
            zf *= 2;

            if (xf >= 1.0) {
                xi++;
                xf--;
            }
            if (yf >= 1.0) {
                yi++;
                yf--;
            }
            if (zf >= 1.0) {
                zi++;
                zf--;
            }
        }
        return r;
    }

    function noiseDetail(lod, falloff) {
        if (lod > 0) {
            perlin_octaves = lod;
        }
        if (falloff > 0) {
            perlin_amp_falloff = falloff;
        }
    }

    function noiseSeed(seed) {
        lcg.setSeed(seed);
        perlin = new Array(PERLIN_SIZE + 1);
        for (var i = 0; i < PERLIN_SIZE + 1; i++) {
            perlin[i] = lcg.rand();
        }
    }

    App.define("math", {
        noise: noise,
        noiseDetail: noiseDetail,
        noiseSeed: noiseSeed,
        DEG2RAD: Math.PI / 180,
        RAD2DEG: 180 / Math.PI,
        clamp: function (value, min, max) {
            return Math.max(min, Math.min(max, value));
        },
        euclideanModulo: function (n, m) {
            return (n % m + m) % m;
        },
        mapLinear: function (x, a1, a2, b1, b2) {
            return b1 + (x - a1) * (b2 - b1) / (a2 - a1);
        },
        lerp: function (x, y, t) {
            return (1 - t) * x + t * y;
        },
        smoothstep: function (x, min, max) {
            if (x <= min) return 0;
            if (x >= max) return 1;

            x = (x - min) / (max - min);

            return x * x * (3 - 2 * x);
        },
        distance: function (x1, y1, x2, y2) {
            return Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1))
        },
        randInt: function (low, high) {
            return low + Math.floor(Math.random() * (high - low));
        },
        randFloat: function (low, high) {
            return low + Math.random() * (high - low);
        },
        randFloatSpread: function (range) {
            return range * (0.5 - Math.random());
        },
        radians: function (degrees) {
            return degrees * _Math.DEG2RAD;
        },
        degrees: function (radians) {
            return radians * _Math.RAD2DEG;
        },
        isPowerOfTwo: function (value) {
            return (value & (value - 1)) === 0 && value !== 0;
        },
        _wrapAngle: function (x) {
            var twoPi = 2.0 * Math.PI;
            while (x > 0.0) {
                x -= twoPi;
            }
            while (x < 0.0) {
                x += twoPi;
            }
            return x;
        },
        lerpAngle: function (a, b, t) {
            var s = 1.0 - t;
            var oa = this._wrapAngle(a);
            var ob = this._wrapAngle(b);

            if (oa < ob) {
                if ((ob - oa) > Math.PI) {
                    oa += 2.0 * Math.PI;
                }
            } else {
                if ((oa - ob) > Math.PI) {
                    ob += 2.0 * Math.PI;
                }
            }
            var ot = ((oa * t) + (ob * s));
            return this._wrapAngle(ot);
        },
        gaussianRandom: function (u, o) {
            var a = Math.random(),
                b = Math.random();
            return Math.sqrt(-2 * Math.log(a)) * Math.cos(2 * Math.PI * b) * u + o;
        }
    })
})